"""
Reads in configuration data from ``config.yml``.

:authors: Riley Baird (OK), Emma Baker (OK)
:created: August 20, 2024
:modified:  May 12, 2025
"""
import difflib
import logging
import os
from collections.abc import Sequence, Collection
from pathlib import Path
from typing import Final, Literal

import attrs
import yaml
from typing_extensions import Optional

from .iterablenamespace import FrozenList
from .config_dataclasses import NG911FeatureClass, NG911Field, NG911Domain, FeatureClassInfoNamespace, FieldInfoNamespace
from .fctypes import *



GDB_ERROR_TABLE_NAME: Final[str] = "TemplateCheckResults"
FEATURE_ATTRIBUTE_ERROR_TABLE_NAME: Final[str] = "FieldValuesCheckResults"

tool_folder_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
config_path = os.path.join(tool_folder_path, "config.yml")

yaml.Node.__match_args__ = ("tag", "value", "start_mark", "end_mark")  # For match-case blocks

def _construct_domain_namespace(loader: yaml.Loader, node: yaml.Node) -> NG911DomainNamespace:
    assert isinstance(node, yaml.SequenceNode)
    sequence = loader.construct_sequence(node, deep=True)
    domains = {x.name: x for x in sequence}
    return NG911DomainNamespace(**domains)

def _construct_field_namespace(loader: yaml.Loader, node: yaml.Node) -> NG911FieldNamespace:
    assert isinstance(node, yaml.SequenceNode)
    sequence = loader.construct_sequence(node, deep=True)
    fields = {x.role: x for x in sequence}
    return NG911FieldNamespace(**fields)

def _construct_feature_class_namespace(loader: yaml.Loader, node: yaml.Node) -> NG911FeatureClassNamespace:
    assert isinstance(node, yaml.SequenceNode)
    sequence = loader.construct_sequence(node, deep=True)
    feature_classes = {x.role: x for x in sequence}
    return NG911FeatureClassNamespace(**feature_classes)

def _construct_feature_class(loader: yaml.Loader, tag: str, node: yaml.Node) -> NG911FeatureClass:
    assert isinstance(node, yaml.MappingNode)
    mapping = loader.construct_mapping(node)
    mapping["fields"] = init_field_namespace(tag, mapping["fields"])
    return NG911FeatureClass(**mapping)


yaml.SafeLoader.add_constructor("!CompleteDomainList", _construct_domain_namespace)
yaml.SafeLoader.add_constructor("!CompleteFieldList", _construct_field_namespace)
yaml.SafeLoader.add_constructor("!FeatureClassList", _construct_feature_class_namespace)

yaml.SafeLoader.add_multi_constructor("!FeatureClass/", _construct_feature_class)


@attrs.frozen(repr=False, str=False)
class _NG911Config(yaml.YAMLObject):
    """Class containing data from ``config.yml``."""
    yaml_tag = "!NG911Config"
    yaml_loader = yaml.SafeLoader

    gdb_info: NG911GeodatabaseInfo
    """Object containing information that applies to the geodatabase or to
    multiple items therein."""

    domains: NG911DomainNamespace
    """Object containing information about the domains specified in the
    Standards. The namespace is designed to facilitate autocompletion."""

    fields: NG911FieldNamespace
    """Object containing information about the fields specified in the
    Standards. The namespace is designed to facilitate autocompletion."""

    feature_classes: NG911FeatureClassNamespace
    """Object containing information about the feature class schemas specified
    in the Standards. The namespace is designed to facilitate autocompletion."""

    @classmethod
    def from_yaml(cls, loader: yaml.SafeLoader, node: yaml.Node):
        assert isinstance(node, yaml.MappingNode)
        return cls(**loader.construct_mapping(node, deep=True))

    # domain_info: list[dict[str, str | CodedValueDict]]
    # """List of dicts with keys 'name', 'description', 'type', 'items'"""

    field_info: dict[str, FieldInfoNamespace] = attrs.field(default=attrs.Factory(
        lambda self: {f.role: f.as_namespace for f in self.fields.values()},
        takes_self=True
    ), init=False)
    """
    Dictionary of all fields with field properties

    .. deprecated:: 3.0.0-alpha.2
       Use :attr:`.fields` instead.
    """

    feature_class_info: dict[str, FeatureClassInfoNamespace] = attrs.field(default=attrs.Factory(
        lambda self: {role: fc.as_namespace for role, fc in self.feature_classes.items()},
        takes_self=True
    ), init=False)
    """
    Dictionary of feature class information: name, geometry_type, fields (list)

    .. deprecated:: 3.0.0-alpha.2
       Use :attr:`.feature_classes` instead.
    """

    required_feature_class_names: FrozenList[str] = attrs.field(default=attrs.Factory(
        lambda self: FrozenList(self.feature_classes[role].name for role in self.feature_classes.keys() if self.feature_classes[role].dataset == self.gdb_info.required_dataset_name),
        takes_self=True
    ), init=False)
    """Returns a list of the **names** (not roles) of feature classes that
    belong in the required feature dataset."""

    optional_feature_class_names: FrozenList[str] = attrs.field(default=attrs.Factory(
        lambda self: FrozenList(self.feature_classes[role].name for role in self.feature_classes.keys() if self.feature_classes[role].dataset == self.gdb_info.optional_dataset_name),
        takes_self=True
    ), init=False)
    """Returns a list of the **names** (not roles) of feature classes that
    are allowed in the optional feature dataset."""

    field_names: FrozenList[str] = attrs.field(default=attrs.Factory(
        lambda self: FrozenList(f.name for f in self.fields.values()), True
    ), init=False)
    """Returns a list of all of **names** (not roles) of all fields."""

    street_fields: FrozenList[NG911Field] = attrs.field(default=attrs.Factory(
        lambda self: FrozenList(((cf := self.fields).premod, cf.predir, cf.pretype, cf.pretypesep, cf.street, cf.streettype, cf.sufdir, cf.sufmod)),
        takes_self=True
    ), init=False)
    """Returns a list of all the :class:`NG911Field` objects relevant to
    computing the full name of a :ng911fc:`road_centerline` feature. Legacy
    fields are not included."""

    street_field_names: FrozenList[str] = attrs.field(default=attrs.Factory(
        lambda self: FrozenList(f.name for f in self.street_fields),
        takes_self=True
    ), init=False)
    """Returns a list of the **names** (not roles) of all the
    :class:`NG911Field` objects relevant to computing the full name of a
    :ng911fc:`road_centerline` feature. Legacy fields are not included."""

    def get_domain_by_name(self, name: str, case_sensitive: bool = True) -> NG911Domain:
        """
        Returns the instance of ``NG911Domain`` from ``self.domains`` with a
        ``name`` attribute equal to *name*.

        :param name: ``name`` attribute of the domain
        :type name: str
        :param case_sensitive: Whether the case of ``name`` must match that of
            the domain, default True
        :type case_sensitive: bool
        :return: Domain with the given name
        :rtype: NG911Domain
        """
        if case_sensitive:
            filtered_config_domains = [d for d in self.domains.values() if d.name == name]
        else:
            filtered_config_domains = [d for d in self.domains.values() if d.name.upper() == name.upper()]
        if len(filtered_config_domains) < 1:
            raise ValueError(f"Config has no domain named '{name}'.")
        elif len(filtered_config_domains) > 1:
            raise ValueError(f"Config has multiple domains named '{name}'.")
        else:
            return filtered_config_domains[0]

    def get_field_by_name(self, name: str, case_sensitive: bool = True) -> NG911Field:
        """
        Returns the instance of :class:`NG911Field` from ``self.fields`` with
        a ``name`` attribute equal to *name*.

        This is an alternative to selecting a field by its ``role`` attribute,
        which, given a field with role "role_value", would normally be done as
        ``self.fields.role_value`` or ``self.fields["role_value"]``.

        :param name: ``name`` attribute of the field
        :type name: str
        :param case_sensitive: Whether the case of ``name`` must match that of
            the field, default True
        :type case_sensitive: bool
        :return: Field with the given name
        :rtype: NG911Field
        """
        if "." in name:
            raise ValueError(f"A field ('{name}' was requested with a dot in the name. Is the relevant feature class part of a join? Remove the join, if applicable, and try again.")
        if case_sensitive:
            filtered_config_fields = [f for f in self.fields.values() if f.name == name]
        else:
            filtered_config_fields = [f for f in self.fields.values() if f.name.upper() == name.upper()]
        if len(filtered_config_fields) < 1:
            raise ValueError(f"Config is missing field named '{name}'.")
        elif len(filtered_config_fields) > 1:
            raise ValueError(f"Config has multiple fields named '{name}'.")
        else:
            return filtered_config_fields[0]

    def get_feature_class_by_name(self, name: str, case_sensitive: bool = True) -> NG911FeatureClass:
        """
        Returns the instance of ``NG911FeatureClass`` from
        ``self.feature_classes`` with a ``name`` attribute equal to *name*.

        This is an alternative to selecting a feature class by its ``role``
        attribute, which, given a feature class with role "role_value", would
        normally be done as ``self.feature_classes.role_value`` or
        ``self.feature_classes["role_value"]``.

        :param name: ``name`` attribute of the feature class
        :type name: str
        :param case_sensitive: Whether the case of ``name`` must match that of
            the feature class, default True
        :type case_sensitive: bool
        :return: Feature class with the given name
        :rtype: NG911FeatureClass
        """
        if case_sensitive:
            filtered_config_fcs = [fc for fc in self.feature_classes.values() if fc.name == name]
        else:
            filtered_config_fcs = [fc for fc in self.feature_classes.values() if fc.name.upper() == name.upper()]
        if len(filtered_config_fcs) < 1:
            raise ValueError(f"Config is missing feature class named '{name}'.")
        elif len(filtered_config_fcs) > 1:
            raise ValueError(f"Config has multiple feature classes named '{name}'.")
        else:
            return filtered_config_fcs[0]

    def _match_names(self, target: Literal["DOMAINS", "FIELDS", "FEATURE_CLASSES"], names: Sequence[str], on_mismatch: Literal["DROP", "ERROR", "NONE"] = "DROP") -> list[str | None]:
        haystack: dict[str, str] = {  # Values to look through for matches; dict of <uppercase name>: <actual name>
            x.name.upper(): x.name for x in {
                "DOMAINS": self.domains,
                "FIELDS": self.fields,
                "FEATURE_CLASSES": self.feature_classes
            }[target].values()
        }
        needles: list[str] = [x.upper() for x in names]
        result: list[str | None] = []
        for name in needles:
            if name in haystack:
                result.append(haystack[name])
            else:
                # Suppress an apparent type checker false-positive:
                # noinspection PyUnreachableCode
                match on_mismatch:
                    case "DROP":
                        continue
                    case "ERROR":
                        raise ValueError(f"Could not match name '{name}' to target '{target}'.")
                    case "NONE":
                        result.append(None)
                    case _:
                        raise ValueError(f"Invalid argument for on_mismatch: '{on_mismatch}'.")
        return result

    def match_domain_names(self, names: Sequence[str], on_mismatch: Literal["DROP", "ERROR", "NONE"] = "DROP") -> list[str | None]:
        """
        Given a case-insensitive sequence of domain names, return a list of the
        corresponding correctly-cased domain names.

        Valid values for *on_mismatch* are:

        - "DROP": Silently skip the name
        - "ERROR": Raise a ValueError
        - "NONE": Append ``None`` to the results

        :param names: Case-insensitive names to match
        :type names: Sequence[str]
        :param on_mismatch: Behavior if no match is found; default "DROP"
        :type on_mismatch: Literal["DROP", "ERROR", "NONE"]
        :return: list[str | None]
        """
        return self._match_names("DOMAINS", names, on_mismatch=on_mismatch)

    def match_field_names(self, names: Sequence[str], on_mismatch: Literal["DROP", "ERROR", "NONE"] = "DROP") -> list[str | None]:
        """
        Given a case-insensitive sequence of field names, return a list of the
        corresponding correctly-cased field names.

        Valid values for *on_mismatch* are:

        - "DROP": Silently skip the name
        - "ERROR": Raise a ValueError
        - "NONE": Append ``None`` to the results

        :param names: Case-insensitive names to match
        :type names: Sequence[str]
        :param on_mismatch: Behavior if no match is found; default "DROP"
        :type on_mismatch: Literal["DROP", "ERROR", "NONE"]
        :return: list[str | None]
        """
        return self._match_names("FIELDS", names, on_mismatch=on_mismatch)

    def match_feature_class_names(self, names: Sequence[str], on_mismatch: Literal["DROP", "ERROR", "NONE"] = "DROP") -> list[str | None]:
        """
        Given a case-insensitive sequence of feature class names, return a list
        of the corresponding correctly-cased feature class names.

        Valid values for *on_mismatch* are:

        - "DROP": Silently skip the name
        - "ERROR": Raise a ValueError
        - "NONE": Append ``None`` to the results

        :param names: Case-insensitive names to match
        :type names: Sequence[str]
        :param on_mismatch: Behavior if no match is found; default "DROP"
        :type on_mismatch: Literal["DROP", "ERROR", "NONE"]
        :return: list[str | None]
        """
        return self._match_names("FEATURE_CLASSES", names, on_mismatch=on_mismatch)

    def get_closest_field_name(self, name: str, fields: Optional[Collection[NG911Field | str]] = None) -> str | None:
        """
        Attempts to find the closest field name to *name* out of those in
        *fields*. Case and underscores are ignored during matching. If *fields*
        is ``None``, all field names are considered. If the match is acceptably
        close, the match is returned (with the correct case). Otherwise,
        returns ``None``.

        :param name: The field name to search for
        :type name: str
        :param fields: Optional list of :class:`NG911Field` objects or field
            names to consider; default None
        :type fields: Optional[Collection[NG911Field | str]]
        :return: The closest field name or ``None``
        :rtype: str | None
        """
        modified_name: str = name.replace("_", "").upper()

        possible_names: set[str]
        if fields is None:
            possible_names = {f for f in self.field_names}
        else:
            possible_names = {f.name if isinstance(f, NG911Field) else f for f in fields}
        possibility_map: dict[str, str] = {
            n.replace("_", "").upper(): n
            for n in possible_names
        }  # <Modified name>: <Actual name>

        return (
            possibility_map[results[0]]
            if (results := difflib.get_close_matches(
                modified_name,
                possibility_map.keys(),
                1,
                0.95
            ))
            else None
        )


config: _NG911Config
"""The single instance of :class:`_NG911Config`, containing data from the
configuration file ``config.yml``."""

with open(config_path, "r") as _f:
    config = yaml.safe_load(_f)

if __name__ == "__main__":
    breakpoint()